/* 
 *  Copyright 2012 CodeMagi, Inc.
 * 
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
package com.codemagi.servlets;

import com.codemagi.database.DataSourceBean;
import com.codemagi.login.AccessException;
import com.codemagi.login.LoginException;
import com.codemagi.login.model.IUser;
import com.codemagi.login.model.User;
import com.codemagi.servlets.model.*;
import com.codemagi.servlets.validation.*;
import com.codemagi.util.*;
import java.io.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;
import org.apache.log4j.Logger;
import org.exolab.castor.jdo.CacheManager;
import org.exolab.castor.jdo.Database;
import org.exolab.castor.jdo.OQLQuery;
import org.exolab.castor.jdo.PersistenceException;
import org.exolab.castor.jdo.QueryResults;

/**
 *  This class provides the main controller and utility methods for use with the Taxonomy system.
 *
 *  @version 1.0
 *  @author August Detlefsen for CodeMagi, Inc.
 */
public class TaxonomyController extends NavigationController {

    static Logger log = Logger.getLogger("com.codemagi.servlets.TaxonomyController");

    //MESSAGES
    protected static String VIEW_MAIN            = "/taxonomy/categories.jsp";
    protected static String VIEW_EDIT_CATEGORY   = "/taxonomy/category_edit.jsp";
    protected static String VIEW_EDIT_TERM       = "/taxonomy/term_edit.jsp";
    protected static String VIEW_DELETE_CATEGORY = "/taxonomy/category_delete.jsp";
    protected static String VIEW_DELETE_TERM     = "/taxonomy/term_delete.jsp";
    protected static String VIEW_LIST_CATEGORIES = "/taxonomy/categories.jsp";
    protected static String VIEW_TERM_MATCHES    = "/taxonomy/term_matches.jsp";

    //VIEWS
    protected static String MSG_CATEGORY_UPDATED = "Category updated successfully.";
    protected static String MSG_CATEGORY_DELETED = "Category deleted successfully.";
    protected static String MSG_TERM_UPDATED     = "Term updated successfully.";
    protected static String MSG_TERM_DELETED     = "Term deleted successfully.";

    //MEMBERS

    /**
     * The permissions group that is allowed to edit terms using this controller. 
     * Override this in subclasses to enable non-superuser editing. 
     */
    protected static String ACCESS_GROUP         = ACCESS_SUPERUSER;

    /**
     * Initializes the servlet and places a reference to it in the application context
     */
    public void init(ServletConfig config)
        throws ServletException {

        super.init(config);

	application.setAttribute("taxonomy", this);

	//set the navigation commands this servlet will respond to 
	typeClasses.put("term", com.codemagi.servlets.model.TermMatches.class);
        typeViews.put("term", VIEW_TERM_MATCHES);
        typeNames.put("term", "term");

	typeClasses.put("category", com.codemagi.servlets.model.Category.class);
        typeViews.put("category", VIEW_EDIT_CATEGORY);
        typeNames.put("category", "category");


        //create validators
        IValidator integerValidator = new IntegerValidator();
        IValidator booleanValidator = new BooleanValidator();
        IValidator stripHtmlValidator = new StripHtmlValidator();
        IValidator notNullValidator = new NotNullValidator();
        IValidator stringLength128Validator  = new StringLengthValidator(128);
        IValidator stringLength256Validator  = new StringLengthValidator(256);
        IValidator stringLength1024Validator = new StringLengthValidator(1024);
        IValidator[] int_notNull_validators =
            { integerValidator, notNullValidator };
        IValidator[] strip_128_validators =
            { stripHtmlValidator, stringLength128Validator };
        IValidator[] strip_256_validators =
            { stripHtmlValidator, stringLength256Validator };
        IValidator[] strip_1024_validators =
            { stripHtmlValidator, stringLength1024Validator };
        IValidator[] strip_notNull_256_validators =
            { stripHtmlValidator, notNullValidator, stringLength256Validator };

        //add input validators
        addValidator("deleteCategory", "returnPage", stripHtmlValidator);
        addValidator("deleteCategory", "id", int_notNull_validators);

        addValidator("category", "id", integerValidator);
        addValidator("category", "name", strip_notNull_256_validators);
        addValidator("category", "description", strip_1024_validators);
        addValidator("category", "allowMultiple", booleanValidator);
        addValidator("category", "allowFreeTags", booleanValidator);

        addValidator("term", "returnPage", stripHtmlValidator);
        addValidator("term", "termId", integerValidator);
        addValidator("term", "categoryId", integerValidator);
        addValidator("term", "termName", strip_notNull_256_validators);
        addValidator("term", "shortName", strip_128_validators);
        addValidator("term", "toolTip", strip_256_validators);

	addValidator("termMatches", "termId", new SetOfIntegersValidator());
	addValidator("termMatches", "termId", notNullValidator);

        addValidator("removeTerm", "returnPage", stripHtmlValidator);
        addValidator("removeTerm", "id", int_notNull_validators); //didn't have notNull in dispatcher, but really should have

    }


    public void service(HttpServletRequest request, HttpServletResponse response) 
	throws ServletException, IOException {

	String nextPage = VIEW_MAIN;

        //get the command or acton passed in from the URL                              
        String command = request.getPathInfo();
	String action  = request.getParameter("acton");
	log.debug("TaxonomyController.service(): acton: " + action + " command: " + command);

	try {
	    //dispatch actions based on request
	    if ("category".equals(action)) {
                nextPage = dispatchCategory(request, response);

	    } else if ("term".equals(action)) {
		nextPage = dispatchTerm(request, response);

	    } else if ("removeTerm".equals(action)) {
		nextPage = dispatchRemoveTerm(request, response);

	    } else if ("termMatches".equals(action)) {
		nextPage = dispatchTermMatches(request, response);

	    } else if ( !Utils.isEmpty(command) ) {
                nextPage = dispatchCommand(request, response);

	    }

        } catch (AccessException ae) {
	    nextPage = handleAccessException(request, response, ae);

        } catch (LoginException le) {
	    nextPage = handleLoginException(request, response, le);

        }

        //forward on
        log.debug("FORWARDING TO: " + nextPage);
        if (!VIEW_NONE.equals(nextPage)) {
	    RequestDispatcher dispatcher = getServletContext().getRequestDispatcher(nextPage);
	    dispatcher.forward(request, response);
	}

        return;
    }

    //DISPATCHERS -------------------------------------------------------------

    /**
     * Dispatches <code>deleteCategory</code> actions. 
     */
    protected String dispatchDeleteCategory(HttpServletRequest request, HttpServletResponse response)
        throws LoginException, AccessException {

        String nextPage = VIEW_LIST_CATEGORIES;

        Database db = null;

        try {
	    String returnList = getReturnPage(request, "");
	    request.setAttribute("returnList", returnList);

	    //check for permissions
	    IUser user = checkLogin(request);

            //get request params
            Integer categoryId = getInteger("id", request);

            db = getJDODatabase();
            db.begin();

            Category category = (Category)db.load(Category.class, categoryId);
            
            //if the user actually pressed submit, delete the category
            //otherwise, send them to confirmation category
            String submit = request.getParameter("submit");
            if (Utils.isEmpty(submit) ) {
                nextPage = VIEW_DELETE_CATEGORY;
                request.setAttribute("category", category);

            } else {

		//perform CSRF token test
		//checkToken(request);

		//did we get any validation errors?
		checkValidation(request);

		//Delete the terms in this category
		OQLQuery oql = db.getOQLQuery(" SELECT t FROM com.codemagi.servlets.model.Term t " + 
					      " WHERE t.category.id = $1 ");
		oql.bind(category.getId());

		QueryResults results = oql.execute();
		while (results.hasMore()) {
		    Term t = (Term)results.next();

		    db.remove(t);
		}

                //delete the category from the DB
                db.remove(category);

                db.commit();

                //success!
                request.setAttribute("error_message", MSG_CATEGORY_DELETED);
            }

        } catch (AppException sne) {
            log.debug("", sne);

            request.setAttribute("error_message", sne.getMessage());

            nextPage = VIEW_DELETE_CATEGORY;

        } catch (PersistenceException e) {
	    handlePersistenceException(request, db, e);

            nextPage = VIEW_MAIN;

        } finally {
            closeJDODatabase(db);
        }

        return nextPage;
    }


    /**
     * Dispatches <code>category</code> actons
     */
    protected String dispatchCategory(HttpServletRequest request, HttpServletResponse response)
        throws LoginException, AccessException {

        log.debug("***** dispatchCategory *****");

        String nextPage   = VIEW_EDIT_CATEGORY;

        Database db = null;

        try {

            //check for login and access
	    checkPermission(request, ACCESS_GROUP);

            //persist
            db = getJDODatabase();
            db.setAutoStore(true);
            db.begin();

            //load the Category to edit
            Integer categoryId = getInteger("id", request);

            Category category = null;
            if (categoryId == null) {
		//we are adding a new category
                category = new Category();

            } else {
                category = (Category)db.load(Category.class, categoryId);

            }

            //if the user actually pressed submit, perform the updates                                    
            //otherwise just forward to the editing page                                              
            String submit = request.getParameter("isSubmit");
            if (Utils.isEmpty(submit) ) {
                nextPage = VIEW_EDIT_CATEGORY;

            } else {

                //get data from form
                String name           = getString("name", request);
                String description    = getString("description", request);
		Boolean allowMultiple = getBoolean("allowMultiple", request);
		Boolean allowFreeTags = getBoolean("allowFreeTags", request);

		//perform CSRF token test
		checkToken(request);

		//did we get any validation errors?
		checkValidation(request);

                category.setName( name );
                category.setDescription( description );
		category.setAllowMultiple( allowMultiple );
		category.setAllowFreeTags( allowFreeTags );

                //create Object in DB if necessary
                if (category.getId() == null) {
                    db.create(category);
                }

                db.commit();

                //success! 
                request.setAttribute("error_message", MSG_CATEGORY_UPDATED);
            }

            request.setAttribute("category", category);

        } catch (AppException se) {
            request.setAttribute("error_message", se.getMessage());

        } catch (PersistenceException e) {
	    handlePersistenceException(request, db, e);

        } finally {
            closeJDODatabase(db);
	}

        return nextPage;
    }


    /**
     * Dispatches <code>term</code> actons
     */
    protected String dispatchTerm(HttpServletRequest request, HttpServletResponse response)
        throws LoginException, AccessException {

        log.debug("***** dispatchTerm *****");

        String nextPage = VIEW_EDIT_TERM;

        Database db = null;

        try {
	    nextPage = getReturnPage(request, VIEW_EDIT_TERM);
	    request.setAttribute("returnPage", nextPage);

            //check for login and access
	    User user = (User)checkPermission(request, ACCESS_GROUP);

            //persist
            db = getJDODatabase();
            db.setAutoStore(true);
            db.begin();

            //load the Term to edit
	    Integer termId     = getInteger("termId", request);
            Integer categoryId = getInteger("categoryId", request);
	    log.debug("termId: " + termId + " categoryId: " + categoryId);
	    	    
            Term term         = null;
	    Category category = null;

            if (termId == null) {
		//we are adding a new term
		log.debug("new term!");
		term = new Term();
		
		category = (Category)db.load(Category.class, categoryId);
		term.setCategory(category);

            } else {
                term     = (Term)db.load(Term.class, termId);
		category = term.getCategory();
            }

	    //set objects into request in case we get any errors
	    log.debug("setting category: " + category);
            request.setAttribute("term", term);
            request.setAttribute("category", category);

            //if the user actually pressed submit, perform the updates
            //otherwise just forward to the editing page
            String submit = request.getParameter("isSubmit");
            if (Utils.isEmpty(submit) ) {
                nextPage = VIEW_EDIT_TERM;

            } else {

		log.debug("submit pressed");

		//perform CSRF token test
		checkToken(request);

                //get data from form
                String name      = getString("termName", request);
                String shortName = getString("shortName", request);
                String toolTip   = getString("toolTip", request);

		log.debug("term name=" + name);

		//did we get any validation errors?
		checkValidation(request);

		//set values in bean
                term.setName( name );
                term.setShortName( shortName );
                term.setToolTip( toolTip );

		//create Object in DB if necessary
                if (term.getId() == null) db.create(term);

                db.commit();

		//expire the performance cache
		CacheManager cm = db.getCacheManager();
		cm.expireCache(Category.class, term.getCategoryId());
		cm.expireCache(Term.class, term.getId());

		//perform any post-transaction application-specific actions  
		afterDispatchTerm(request, response);

                //success! 
                request.setAttribute("error_message", MSG_TERM_UPDATED);
		application.getContext("/").setAttribute("error_message." + user.getSsoHash(), MSG_TERM_UPDATED);
		request.setAttribute("dispatchTermSuccess", "true");

		//redirect back to category edit screen
		nextPage = "/category/" + term.getCategoryId();
		log.debug("About to redirect to: " + nextPage);
		response.sendRedirect(nextPage);
		nextPage = VIEW_NONE;
            }

        } catch (AppException se) {
            request.setAttribute("error_message", se.getMessage());

        } catch (PersistenceException e) {
	    handlePersistenceException(request, db, e);

	} catch (IOException ioe) {
	    log.debug("Exception trying to redirect to: " + nextPage, ioe);
	    nextPage = VIEW_ROOT;

        } finally {
            closeJDODatabase(db);
	}

        return nextPage;
    }


    /**
     * Dispatches <code>removeTerm</code> actons
     */
    protected String dispatchRemoveTerm(HttpServletRequest request, HttpServletResponse response)
        throws LoginException, AccessException {

        log.debug("***** dispatchRemoveTerm *****");

        String nextPage = "/taxonomy/categories.jsp";

	Database db = null;

        try {
	    nextPage = getReturnPage(request, VIEW_EDIT_CATEGORY);
	    request.setAttribute("returnPage", nextPage);

            //check for login and access
	    checkPermission(request, ACCESS_GROUP);

            //persist
            db = getJDODatabase();
	    //            db.setAutoStore(true);
            db.begin();

            //load the Term to remove
            Integer termId = getInteger("id", request);
	    
	    //perform CSRF token test
	    checkToken(request);

	    //did we get any validation errors?
	    checkValidation(request);

	    //delete to support returnPage
	    //nextPage = "/taxonomy/categories.jsp";//taxonomy?acton=category&id=" + categoryId;

	    //load the term
            Term term = (Term)db.load(Term.class, termId);

	    //delete the term from the category
	    Category category = (Category)db.load(Category.class, term.getCategory().getId());
	    category.removeTerm(term);
	    
	    //delete the term from any nodes its attached to
	    DataSourceBean dataBean = new DataSourceBean( application.getInitParameter("database") );
	    dataBean.setQuery(" DELETE from term_node_xref WHERE term_id = " + term.getId() );
	    dataBean.executeQuery();

	    //actually delete the term
	    db.remove(term);

	    db.commit();
	    
	    //expire the performance cache
	    CacheManager cm = db.getCacheManager();
	    cm.expireCache(Category.class, category.getId());
	    cm.expireCache(Term.class, term.getId());

	    Class[] nodeArray = { Node.class };
	    cm.expireCache( nodeArray );

	    //perform any post-transaction application-specific actions
	    afterDispatchRemoveTerm(request, response);
	    
	    //success! 
	    request.setAttribute("category", category);
	    request.setAttribute("error_message", MSG_TERM_DELETED);
		    
	} catch (AppException se) {
	    request.setAttribute("error_message", se.getMessage());
	    
	} catch (PersistenceException e) {
	    handlePersistenceException(request, db, e);
	    
	} finally {
	    closeJDODatabase(db);
	}
	
	return nextPage;
    }


    /**
     * Dispatches <code>termMatches</code> actons: Returns a list of Nodes that match multiple Terms
     */
    protected String dispatchTermMatches(HttpServletRequest request, HttpServletResponse response)
        throws LoginException, AccessException {

        log.debug("***** dispatchTermMatches *****");

        String nextPage = VIEW_TERM_MATCHES;

        Database db = null;

        try {
            //persist
            db = getJDODatabase();
            db.begin();

            //load the Terms to match
	    Set<Integer> termIds = (Set<Integer>)getSet("termId", request);

	    checkValidation(request);

	    //data structure to hold the matching nodes
	    ArrayList matches = new ArrayList();

	    log.debug("Term IDs: " + CollectionUtils.commaDelimit(termIds));

	    DataSourceBean dataBean = new DataSourceBean( application.getInitParameter("database") );
	    dataBean.setQuery(" SELECT DISTINCT n.node_id, n.class_name FROM node n, term_node_xref tnx " + 
			      " WHERE n.node_id = tnx.node_id AND tnx.term_id IN (" + CollectionUtils.commaDelimit(termIds) + ")");
	    dataBean.executeQuery();

	    while (dataBean.hasMoreRows()) {
		dataBean.nextRow();

		Integer nodeId   = convertInteger( dataBean.getItem("node_id") );
		String className = dataBean.getItem("class_name");

		try {
		    //load the Node and add it to matches
		    Node n = (Node)db.load(Class.forName(className), nodeId, Database.ReadOnly);
		    matches.add(n);
		} catch (Exception e) {
		    //log the error but take no action
		    log.debug("error loading class in termMatches", e);
		}
	    }
	    
	    //build up the output
	    TermMatches output = new TermMatches();
	    output.setMatches(matches);

	    //success! 
	    request.setAttribute("term", output);
	    nextPage = VIEW_TERM_MATCHES;
	    
        } catch (AppException se) {
            request.setAttribute("error_message", se.getMessage());

        } catch (PersistenceException e) {
	    handlePersistenceException(request, db, e);

        } finally {
            closeJDODatabase(db);
	}

        return nextPage;
    }


    // METHODS --------------------------------------------------------

    /**
     * Returns a List of Nodes that match the passed Term.
     */
    public List<Node> getMatches(Term term) {

	return getMatches(term, new Node());

	/*
	List<Node> output = new ArrayList<Node>();

	if (term == null || term.getId() == null) return output;

	Database db = null;

	try {
	    db = getJDODatabase();
	    db.begin();

	    OQLQuery oql = db.getOQLQuery(" SELECT n FROM com.codemagi.servlets.model.Node n " + 
					  " WHERE n.term.id = $1 ");
	    oql.bind(term.getId());

	    QueryResults results = oql.execute(Database.ReadOnly);

	    while (results.hasMore()) {
		Node n = (Node)results.next();

		output.add(n);
	    }

	} catch (PersistenceException pe) {
	    handlePersistenceException(db, pe);

	} finally {
	    closeJDODatabase(db);

	}

	return output;
	*/
    }


    /**
     * Returns a List of Nodes that match the passed Term.
     */
    public <T> List<T> getMatches(Term term, T type) {

	List<T> output = new ArrayList<T>();

	if (term == null || term.getId() == null) return output;

	Database db = null;

	try {
	    db = getJDODatabase();
	    db.begin();

	    OQLQuery oql = db.getOQLQuery(" SELECT n FROM " + type.getClass().getName() + " n " + 
					  " WHERE n.terms.id = $1 ");
	    oql.bind(term.getId());

	    QueryResults results = oql.execute(Database.ReadOnly);

	    while (results.hasMore()) {
		output.add( (T)results.next() );
	    }

	} catch (PersistenceException pe) {
	    handlePersistenceException(db, pe);

	} finally {
	    closeJDODatabase(db);

	}

	return output;
    }


    /**
     * Returns a List of Nodes that match the Term with the pased alias
     */
    public List<Node> getMatches(String termAlias) {
	Term term = getTerm(termAlias);

	return getMatches(term);
    }

    /**
     * Loads a Category and related terms by <b>category ID</b>. Returns a new Category on any database exceptions.
     */
    public Category getCategory(Integer categoryId) {

	Category output = new Category();

	//sanity check 
	if (categoryId == null) return output;

	Database db = null;

	try {
	    //persist                                                                                   
            db = getJDODatabase();
            db.begin();

	    output = (Category)db.load(Category.class, categoryId, Database.ReadOnly);

        } catch (PersistenceException e) {
	    handlePersistenceException(db, e);

        } finally {
            closeJDODatabase(db);
        }

        return output;
    }


    /**
     * Loads a Category and related terms based onthe category <b>alias</b>. Returns a new Category on any database exceptions. 
     */
    public Category getCategory(String alias) {

	log.debug("TaxonomyController.getCategory(" + alias + ")");

	Category output = new Category();

	//sanity check 
	if (Utils.isEmpty(alias)) return output;

	Database db = null;

	try {
	    //persist                                                                                   
            db = getJDODatabase();

	    CacheManager cm = db.getCacheManager();
	    Class[] type    = { Category.class };
            cm.expireCache(type);

            db.begin();

	    OQLQuery oql = db.getOQLQuery(" SELECT c FROM com.codemagi.servlets.model.Category c WHERE c.alias = $1 ");
	    oql.bind(alias);

	    QueryResults results = oql.execute(Database.ReadOnly);
	    if (results.hasMore()) output = (Category)results.next();

	    log.debug("     output: " + output);

        } catch (PersistenceException e) {
	    log.debug("Exception during getTerm() " + e);
	    handlePersistenceException(db, e);

        } finally {
            closeJDODatabase(db);
        }

        return output;
    }


    /**
     * Creates a new Category with the passed parameters.
     */
    protected Category createCategory(String name, String alias, String desc, Boolean allowMultiple) {
        return createCategory(name, alias, desc, allowMultiple, new Boolean(false));
    }

    /**
     * Creates a new Category with the passed parameters.
     */
    protected Category createCategory(String name, String alias, String desc, Boolean allowMultiple, Boolean allowFreeTags) {

	Category output = new Category();

	//sanity check 
	if (Utils.isEmpty(alias) || Utils.isEmpty(name)) return output;

	Database db = null;

	try {
	    //persist                                                                                   
            db = getJDODatabase();
            db.begin();

	    output = new Category();
	    output.setName(name);
	    output.setAlias(alias);
	    output.setDescription(desc);
	    output.setAllowMultiple(allowMultiple);
	    output.setAllowFreeTags(allowFreeTags);

	    db.create(output);
	    db.commit();

        } catch (PersistenceException e) {
	    handlePersistenceException(db, e);

        } finally {
            closeJDODatabase(db);
        }

        return output;
    }


    //TAXONOMY UTILITY METHODS ------------------------------------------------

    /**
     * Sets the Terms that should be added to a Node. Any terms not included in the input will be removed fom the Node.
     */
    public static void setTerms(Node node, HttpServletRequest request, Database db)
        throws AppException, PersistenceException {

	setTerms(node, request, db, true);
    }

    /**
     * Sets the Terms that should be added to a Node. 
     *
     * @param node            The Node to set Terms into
     * @param request         The servlet request
     * @param db              Initialized Database instance
     * @param removeOldTerms  If true, old terms attached to this category will be removed prior to adding new Terms
     */
    public static void setTerms(Node node, HttpServletRequest request, Database db, boolean removeOldTerms)
        throws AppException, PersistenceException {

	log.debug("setTerms() node: " + node + " removeOldTerms: " + removeOldTerms);

	//sanity check
	if (node == null || request == null || db == null) return;

	List taxParameters = getParametersByName(request, "taxonomy_", false);
	log.debug("    taxParameters: " + taxParameters);

	Iterator i = taxParameters.iterator();
	while (i.hasNext()) {
	    String categoryParamName = (String)i.next();
	    log.debug("    setTerms() category: " + categoryParamName);
	    String sCategoryId = categoryParamName.substring(9);
	    log.debug("        category ID: " + sCategoryId);
	    Integer categoryId = convertInteger(sCategoryId);

	    //remove all terms in this category from the Node
	    if (removeOldTerms) node.removeTerms(categoryId);

	    List<String> termIds = FormUtils.getSelectValues(request, categoryParamName);
	    if (termIds.size() == 1) {
		//see if we have multiple comma separated terms
		String[] values = termIds.get(0).split(";");

                if (values.length > 0) termIds = Arrays.asList( values );
	    }

	    Iterator j = termIds.iterator();
	    while (j.hasNext()) {

		String param = (String)j.next();

		Term t = getTerm(param, categoryId, db);

		if (t == null) continue;

		node.addTerm(t);

	    }
	}
    }


    /**
     * Sets the Terms that should be added to a Node. Any terms not included in the input will be removed fom the Node.
     */
    public static void setTerms(Node node, Map params, Database db)
        throws AppException, PersistenceException {

	setTerms(node, params, db, true);
    }

    /**
     * Sets the Terms that should be added to a Node. 
     *
     * @param node            The Node to set Terms into
     * @param request         The servlet request
     * @param db              Initialized Database instance
     * @param removeOldTerms  If true, old terms attached to this category will be removed prior to adding new Terms
     */
    public static void setTerms(Node node, Map params, Database db, boolean removeOldTerms)
        throws AppException, PersistenceException {

	log.debug("setTerms() node: " + node + " removeOldTerms: " + removeOldTerms);

	//sanity check
	if (node == null || params == null || db == null) return;

	List taxParameters = getParametersByName(params, "taxonomy_", false);
	log.debug("    taxParameters: " + taxParameters);

	Iterator i = taxParameters.iterator();
	while (i.hasNext()) {
	    String categoryParamName = (String)i.next();
	    log.debug("    setTerms() category: " + categoryParamName);
	    String sCategoryId = categoryParamName.substring(9);
	    log.debug("        category ID: " + sCategoryId);
	    Integer categoryId = convertInteger(sCategoryId);

	    //remove all terms in this category from the Node
	    if (removeOldTerms) node.removeTerms(categoryId);

	    //do we have a single param or a list? 
	    Collection termIds = new ArrayList<String>();
	    Object value       = params.get("taxonomy_" + categoryId);
	    if (value instanceof String) {
		String[] values = ((String)value).split(";");
		
		if (values.length > 0) termIds.addAll( Arrays.asList( values ) );

	    } else {
		termIds = (ArrayList)value;
	    }

	    Iterator j = termIds.iterator();
	    while (j.hasNext()) {

		Object o = j.next();
		log.debug("                term: " + o);
		String param = (String)o;

		Term t = getTerm(param, categoryId, db);

		if (t == null) continue;

		node.addTerm(t);
	    }
	}
    }


    //HELPER METHODS ---------------------------------------------------------------------

    /** 
     * Returns the term with the passed alias.
     */
    public Term getTerm(String termAlias) {

	Database db = null; 

	try {
	    db = getJDODatabase();
	    db.begin();

	    OQLQuery oql = db.getOQLQuery(" SELECT t FROM com.codemagi.servlets.model.Term t " + 
					  " WHERE t.alias = $1 ");
	    oql.bind(termAlias);

	    QueryResults results = oql.execute(Database.ReadOnly);

	    if (results.hasMore()) return (Term)results.next();

	} catch (PersistenceException pe) {
	    handlePersistenceException(db, pe);

	} finally {
	    closeJDODatabase(db);

	}

	return new Term();
    }


    /**
     * Returns a Term matching the passed String, either by Integer ID, or String match. 
     * Will attempt to create Terms if term.getCategory().allowFreeTags() == true
     *
     * Returns null if the term could not be found or created. 
     */
    private static Term getTerm(String param, Integer categoryId, Database db) throws PersistenceException {
	
	//sanity check
	if (Utils.isEmpty(param)) return null;
	
	//first try to get the term by ID
	try {
	    Integer termId = convertInteger(param);
	    log.debug("            term ID: " + termId);
	    
	    Term term = (Term)db.load(Term.class, termId);
	    return term;
	    
	} catch (AppException ae) {
	    log.debug("             -NOT INTEGER");
	    //no-op
	}
	
	//next try to load an existing term by name
	log.debug("                term NAME: '" + param + "'");

	//clean up input 
	param = StringUtils.stripHtmlRegex(param);         //strip tags 
	param = StringUtils.clean(param);                  //white space                

	//do we have anything left? 
	if (Utils.isEmpty(param)) return null;

	OQLQuery oql = db.getOQLQuery(" SELECT t FROM com.codemagi.servlets.model.Term t " + 
				      " WHERE upper(t.name) LIKE $1 AND t.category.id = $2 ");
	oql.bind(StringUtils.toUpperCase(param));
	oql.bind(categoryId);
	
	QueryResults results = oql.execute();
	if (results.hasMore()) {
	    Term term = (Term)results.next();
	    log.debug("Found term in DB: " + term);
	    
	    return term;
	    
	} else {
	    //first load the category and check for permission to add free tags
	    Category category = (Category)db.load(Category.class, categoryId);
	    
	    if (!category.allowFreeTags()) return null;
	    
	    //if we allow user input then create a new term
	    log.debug("Adding new term: '" + param + "'");
	    
	    Term term = new Term();
	    term.setCategory(category);
	    term.setName(param);
	    
	    db.create(term);
	    
	    return term;
	}
    }


    /**
     * Returns a Term matching the passed String, either by Integer ID, or String match. 
     * Will attempt to create Terms if term.getCategory().allowFreeTags() == true
     *
     * Returns null if the term could not be found or created. 
     */
    public static Term getTerm(String param, String categoryAlias, Database db) throws PersistenceException {
	
	//sanity check
	if (Utils.isEmpty(param)) return null;
	
	//first try to get the term by ID
	try {
	    Integer termId = convertInteger(param);
	    log.debug("            term ID: " + termId);
	    
	    Term term = (Term)db.load(Term.class, termId);
	    return term;
	    
	} catch (AppException ae) {
	    log.debug("             -NOT INTEGER");
	    //no-op
	}
	
	//next try to load an existing term by name
	log.debug("                term NAME: '" + param + "'");

	//clean up input 
	param = StringUtils.stripHtmlRegex(param);         //strip tags 
	param = StringUtils.clean(param);                  //white space                

	//do we have anything left? 
	if (Utils.isEmpty(param)) return null;

	OQLQuery oql = db.getOQLQuery(" SELECT t FROM com.codemagi.servlets.model.Term t " + 
				      " WHERE upper(t.name) LIKE $1 AND t.category.alias = $2 ");
	oql.bind(StringUtils.toUpperCase(param));
	oql.bind(categoryAlias);
	
	QueryResults results = oql.execute();
	if (results.hasMore()) {
	    Term term = (Term)results.next();
	    log.debug("Found term in DB: " + term);
	    
	    return term;
	    
	} else {
	    //first load the category and check for permission to add free tags
	    OQLQuery oql2 = db.getOQLQuery(" SELECT c FROM com.codemagi.servlets.model.Category c WHERE c.alias = $1 ");
	    oql2.bind(categoryAlias);

	    QueryResults results2 = oql2.execute();
	    if (!results2.hasMore()) return null;  //category does not exist. fail.

	    Category category = (Category)results2.next();

	    if (!category.allowFreeTags()) return null;
	    
	    //if we allow user input then create a new term
	    log.debug("Adding new term: '" + param + "'");
	    
	    Term term = new Term();
	    term.setCategory(category);
	    term.setName(param);
	    
	    db.create(term);
	    
	    return term;
	}
    }

    /** 
     * Returns Terms matching the passed Node ID with the specified category alias. 
     */
    public List<Term> getTerms(Integer nodeId, String alias) {
	if (nodeId == null) return new ArrayList<Term>();

	Database db = null;

	try {
	    db = getJDODatabase();
	    db.begin();

	    return getTerms(nodeId, alias, db);

	} catch (PersistenceException pe) {
	    handlePersistenceException(db, pe);
	    
	} finally {
	    closeJDODatabase(db);
	}

	return new ArrayList<Term>();
    }

    /** 
     * Returns Terms matching the passed Node ID with the specified category alias.
     */
    public List<Term> getTerms(Integer nodeId, String alias, Database db) throws PersistenceException {
	if (nodeId == null || db == null) return new ArrayList<Term>();

	Node node = (Node)db.load(Node.class, nodeId, Database.ReadOnly);

	return node.getTerms(alias);
    }


    //HOOKS ----------------------------------------------------------------------------------------

    /**
     * Override this method to perform application-specific post-transaction actions for <code>term</code> actons.
     */
    protected void afterDispatchTerm(HttpServletRequest request, HttpServletResponse response)  
        throws AppException {  

    }


    /**
     * Override this method to perform application-specific post-transaction actions for <code>removeTerm</code> actons.
     */
    protected void afterDispatchRemoveTerm(HttpServletRequest request, HttpServletResponse response)  
        throws AppException {  

    }

}
